iOS中常见的八种锁

为什么需要锁

在iOS中相信大家都用过多线程,多线程带来的好处显而易见,但是我们需要关注一下多线程有可能带来的问题。假设我们有一个这样的场景,我们有两条线程A和线程B,A线程做的事情是修改这个对象之后读取这个对象的数据,这个时候B线程可能也在修改这个对象。这个时候有两种情况(取决于B线程修改对象的时机):

  1. 正常的情况,A线程修改对象以及读取对象之后,B线程才开始修改这个对象。
  2. 异常的情况,A线程修改对象之后,B线程立刻修改了这个帝乡,然后A线程读取对象。这个时候A线程读取到的数据就出错了。

这就是我们常说的Data race,当两个线程同时在访问修改同一个块内存的时候,就有可能得到意想不到的结果。

基于上面的前提,我们在出现了用来解决问题的方法。下面我们就来说说iOS中的锁。

在ibireme写的不再安全的OSSpinLock中给出了常用的锁的性能如下所示:

在解释下面的锁之前,我们先说说两种类型的锁,一种是自旋锁,一种是互斥锁。

按照功能来区分锁

互斥锁

互斥锁是为了保护一个临界区或者资源不能同时被多个小城访问。当临界区加上互斥锁以后,其他的调用方不能获得锁,只有当互斥锁的持有方释放锁之后其他调用方才能获得锁。
如果调用方在获得锁的时候发现互斥锁已经被其他方持有,那么该调用方只能进入睡眠状态,这样不会占用CPU资源。但是会有时间的消耗,系统的运行时基于CPU时间调度的,每次线程可能有100ms的运行时间,频繁的CPU切换也会消耗一定的时间。

自旋锁

自旋锁和互斥锁相似,但是自旋锁不会引起休眠,当自旋锁被别的线程锁定的时候,那么调用方会一直处于等待的状态,用一种生活化的例子来说就像是上厕所,当你要上厕所发现里面已经有人的时候,你就会一直等在外面,直到他出来你就立刻抢占厕所。
由于调用方会一直循环看该自旋锁的的保持者是否已经释放了资源,所以总的效率来说比互斥锁高。但是自旋锁只用于短时间的资源访问,如果不能短时间内获得锁,就会一直占用着CPU,造成效率低下。

常见的锁的类型

OSSpinLock

OSSpinLock是自旋锁,也正是由于它是自旋锁,所以容易发生优先级反转的问题。在ibireme的文章中已经写到,当一个低优先级线程获得锁的时候,如果此时一个高优先级的系统到来,那么会进入忙等状态,不会进入睡眠,此时会一直占用着系统CPU时间,导致低优先级的无法拿到CPU时间片,从而无法完成任务也无法释放锁。除非能保证访问锁的线程全部处于同一优先级,否则系统所有的自旋锁都会出现优先级反转的问题。现在苹果的OSSpinLock已经被替换成os_unfair_lock
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

dispatch_semaphore

dispatch_semaphore主要提供了三个函数:

1
2
3
dispatch_semaphore_create(long value);//创造信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); //等待信号
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//发送信号

dispatch_semaphore是GCD用来同步的一种方式,dispatch_semephore_create方法用户创建一个dispatch_semephore_t类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。
dispatch_semaphore_wait()方法是等待一个信号量,该方法会判断signal的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和NSCondition一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且dispatch_semaphore_signal释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。

pthread-mutex

pthread-mutex是互斥锁,互斥锁与信号量的机制非常相似,不会处于忙等状态,而是会阻塞线程并休眠。

pthread-mutex提供了几个常用的方法

1
2
3
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);//初始化锁
int pthread_mutex_lock(pthread_mutex_t *); //加锁
int pthread_mutex_unlock(pthread_mutex_t *); //解锁

pthread_mutex_init方法用来初始化一个锁,需要传入一个pthread_mutex_t的对象,并且需要设置互斥锁的类型。互斥锁有四种类型:

1
2
3
4
PTHREAD_MUTEX_NORMAL : 默认值普通锁,当一个线程加锁以后,其他线程进入按照优先顺序进入等待队列,并且解锁的时候按照先入先出的方式获得锁。
PTHREAD_MUTEX_ERRORCHECK : 检错锁,当同一个线程获得同一个锁的时候,则返回EDEADLK,否则与普通锁处理一样。
PTHREAD_MUTEX_RECURSIVE : 递归锁。这里有别于上面的检错锁,同一个线程可以递归获得锁,但是加锁和解锁必须要一一对应。
PTHREAD_MUTEX_DEFAULT : 适应锁,等待解锁之后重新竞争,没有等待队列。

NSLock

NSLock遵循NSLocking协议,同时也是互斥锁,提供了lock和unlock方法来进行加锁和解锁。
NSLock内部是封装了pthread_mutext,类型是PTHREAD_MUTEXT_ERRORCHECK,它会损失一定的性能换来错误提示。

NSCondition

NSCondition是封装了一个互斥锁和信号量,它把前者的lock以及后者的wait/signal统一到NSCondition对象中,是基于条件变量pthread_cond_t来实现的,和信号量相似,如果当前线程不满足条件,那么就会进入睡眠状态,等待其他线程释放锁或者释放信号之后,就会唤醒线程。类似于生产者和消费者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//消费者
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (!array.count) {
[lock wait];
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});

//生产者
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
[lock unlock];
});

NSRecursiveLock

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中

NSRecursiveLock内部是通过pthread_mutex_lock来实现的,在内部会判断锁的类型,如果是递归锁,就允许递归调用,内部仅仅是将计数器+1。当调用unlock的时候,就将计数器减1。NSRecursiveLock内部使用的pthread_mutex_t的类型是PTHREAD_MUTEXT_RECURSIVE

NSConditionLock

NSConditonLock 是借助NSCondition,本质上是生产者-消费者模式,NSConditonLock内部持有了一个NSCondition对象和_condition_value属性,当调用- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;初始化的时候会传入一个condition参数,该参数会赋值_condition_value属性。

  • NSConditionLockr中,对应的消费者就是- (void)lockWhenCondition:(NSInteger)condition;方法,首先会调用[condition lock],然后开始进入阻塞状态,如果condition=_condition_value,那么就会休眠,直到代码调用- (void)unlockWithCondition:(NSInteger)condition;才会唤起

  • - (void)unlockWithCondition:(NSInteger)condition;就是对应的生产者方法,内部会设置condition=_contion_value,并且发送广播告诉所有的消费者,表示生产完成,然后调用[condition unlock]释放锁。

@synchronized

@synchronized是OC层面上的锁,是所有的锁之中性能最差的。
@synchronized后面紧跟一个OC对象,实际上是将这个对象当做锁来使用。这是通过一个哈希表来实现的,OC在底层维护了一个互斥锁的数组,通过对象的哈希值去得到对象的互斥锁。
具体的实现原理可以参考萧玉大神的这篇文章: 关于 @synchronized,这儿比你想知道的还要多

总结

经过上面的分析我们知道锁的性能由高到低分别是
OSSpinLock(已经不推荐使用)->dispatch_semaphore->pthread_mutext->NSLock->NSCondition->NSRecursiveLock->NSConditonLock->@synchronized

我们再来梳理一下它们的关系:

  1. dipatch_semaphore是GCD同步的一种方式,通过dispatch_semaphore_t信号量来实现。
    2.pthread_mutex是互斥锁,提供了四种不同类型,不会像自旋锁一样忙等,而是会进入休眠等待。
    3.NSLock是封装了prthread_mutex,锁的类型是PTHREAD_MUTEX_ERRORCHECK,也就是当同一个线程获得同一个锁的时候,会返回错误。
    4.NSCondition是基于条件变量pthread_cond_t实现的,和信号量相似,当不满足条件的时候就会进入休眠等待,知道condition对象发出signal信号,才会被唤醒执行。
    5.NSRecursiveLock是递归锁,同样是封装了pthread_mutex来实现,但是锁的类型是PTHREAD_MUTEX_RECURSIVE,允许统一递归获得锁,但是要注意加锁和解锁要一一对应。
    6.NSConditionLock是基于NSCondition实现的,同样也是生产者和消费者模式。
    7.@synchronized是OC层面的锁,传入一个OC对象,通过对象的哈希值来作为标识符得到互斥锁,存入到一个数组里面。

参考

深入理解iOS中的锁
iOS中保证线程安全的几种方式与性能对比
iOS 常见知识点(三):Lock
不再安全的OSSpinLock

-------评论系统采用disqus,如果看不到需要翻墙-------------